package com.finger.gulimall.product.service.impl;

import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.TypeReference;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.finger.common.utils.PageUtils;
import com.finger.common.utils.Query;
import com.finger.gulimall.product.dao.CategoryDao;
import com.finger.gulimall.product.entity.CategoryEntity;
import com.finger.gulimall.product.service.CategoryBrandRelationService;
import com.finger.gulimall.product.service.CategoryService;
import com.finger.gulimall.product.vo.Catelog2Vo;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.cache.annotation.Caching;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.StringUtils;


@Service("categoryService")
public class CategoryServiceImpl extends ServiceImpl<CategoryDao, CategoryEntity> implements CategoryService {

	@Autowired
	private CategoryBrandRelationService categoryBrandRelationService;

	@Autowired
	private StringRedisTemplate redisTemplate;

	@Autowired
	private RedissonClient redissonClient;

	@Override
	public PageUtils queryPage(Map<String, Object> params) {
		IPage<CategoryEntity> page = this.page(new Query<CategoryEntity>().getPage(params), new QueryWrapper<CategoryEntity>());

		return new PageUtils(page);
	}


	// 查找商品种类的树形菜单
	//    @Cacheable(value = "categoryTree",keyGenerator = "keyGenerator")
	@Override
	public List<CategoryEntity> listForTree() {

		// 查找所有的商品分类信息
		List<CategoryEntity> allEntities = baseMapper.selectList(new QueryWrapper<CategoryEntity>().last("limit 0,1450"));

		// 使用流，递归方式查找查找全部的一级商品分类，并组装一级分类
		List<CategoryEntity> allCategory1Menu = allEntities.stream()
				// 过滤查找出全部的一级分类
				.filter(entity -> entity.getParentCid() == 0).sorted((menu1, menu2) -> {
					return (menu1.getSort() == null ? 0 : menu1.getSort()) - (menu2.getSort() == null ? 0 : menu2.getSort());
					// 最终把结果收集成为集合
				}).collect(Collectors.toList());
		for (CategoryEntity category1Menu : allCategory1Menu) {
			category1Menu.setChildren(findChildren(category1Menu, allEntities));
		}

		return allCategory1Menu;

        /*
        之前用另一种方式实现而编写的代码，不忍心删除
        List<CategoryEntity> allCategory1Menu = allEntities.stream()
                .filter(entity -> entity.getParentCid() == 0)
                .sorted((menu1,menu2)-> (menu1.getSort()==null ? 0:menu1.getSort()) - (menu2.getSort()==null?0:menu2.getSort()) )
                .collect(Collectors.toList());

        for (CategoryEntity category1Menu : allCategory1Menu) {

            Integer catId = category1Menu.getCatId();

            // 该一级菜单下的二级菜单
            List<CategoryEntity> allCategory2Menu = allEntities.stream().filter(entity -> entity.getParentCid() == catId).collect(Collectors.toList());

            category1Menu.setChildren(allCategory2Menu);

        }

        for (CategoryEntity category1Menu : allCategory1Menu) {
            List<CategoryEntity> children = category1Menu.getChildren();
            if(children!=null){
                for (CategoryEntity child : children) {
                    Integer catId = child.getCatId();

                    ArrayList<CategoryEntity> objects = new ArrayList<>();

                    for (CategoryEntity allEntity : allEntities) {
                       if(allEntity.getParentCid().equals(catId)){
                           objects.add(allEntity);
                       }
                    }

                    child.setChildren(objects);

                }
            }

        }
        return allCategory1Menu;
        */
	}

	// 查找商品种类的树形菜单 使用数组方式
	@Override
	public List<CategoryEntity> listWithTree() {
		// 查找所有商品分类信息
		List<CategoryEntity> allEntities = baseMapper.selectList(new QueryWrapper<CategoryEntity>().last("limit 0,10000"));

		List<CategoryEntity> level1Menu = new ArrayList<>();

		for (CategoryEntity categoryEntity1 : allEntities) {
			if (categoryEntity1.getParentCid() == 0) {
				// 添加一级分类
				level1Menu.add(categoryEntity1);

				ArrayList<CategoryEntity> chirdren1 = new ArrayList<>();

				categoryEntity1.setChildren(chirdren1);

				for (CategoryEntity categoryEntity2 : allEntities) {

					if (categoryEntity1.getCatId() == categoryEntity2.getParentCid()) {

						ArrayList<CategoryEntity> chirdren2 = new ArrayList<>();
						categoryEntity2.setChildren(chirdren2);
						chirdren1.add(categoryEntity2);
						for (CategoryEntity categoryEntity3 : allEntities) {

							if (categoryEntity3.getParentCid() == categoryEntity2.getCatId()) {
								ArrayList<CategoryEntity> chirdren3 = new ArrayList<>();
								categoryEntity3.setChildren(chirdren3);
								chirdren2.add(categoryEntity3);

								for (CategoryEntity categoryEntity4 : allEntities) {

									if (categoryEntity3.getCatId() == categoryEntity4.getParentCid()) {

										chirdren3.add(categoryEntity4);

									}
								}
							}
						}
					}
				}
			}
		}

		return level1Menu;
	}


	// 自定义根据菜单Id集合批量删除菜单

	/**
	 * @param asList
	 * @CacheEvict 该方法执行移除缓存中的哪条数据，key指明缓存分区,allEntries = true是清除该缓存分区下的全部内容，key是指定要清除的数据
	 * @Caching 可以把多个缓存条件整合到一起
	 */
	@Caching(evict = {@CacheEvict(value = "category", key = "'getLevelOneList'"), @CacheEvict(value = "category", key = "'getLevelTwoList'")})
	@Override
	public void removeBachIds(List<Integer> asList) {
		// TODO 删除之前先查找一下id有没有被其他地方引用
		baseMapper.deleteBatchIds(asList);

	}

	@Override
	public Long[] getPaths(Long catId) {
		List<Long> paths = new ArrayList<>();
		findParentId(catId, paths);
		Collections.reverse(paths);
		return paths.toArray(new Long[paths.size()]);
	}

	@Caching(evict = {@CacheEvict(value = "category", key = "'getLevelOneList'"), @CacheEvict(value = "category", key = "'getLevelTwoList'")})
	@Transactional
	@Override
	public void updateDetails(CategoryEntity category) {
		baseMapper.updateById(category);
		// 如果更新的属性名不为空
		if (!StringUtils.isEmpty(category.getName())) {
			categoryBrandRelationService.updateCategoryDetails(category.getCatId(), category.getName());
		}

	}

	@Caching(evict = {@CacheEvict(value = "category", key = "'getLevelOneList'"), @CacheEvict(value = "category", key = "'getLevelTwoList'")})
	@Override
	public void saveCategory(CategoryEntity category) {
		baseMapper.insert(category);
	}

	/**
	 * 1、每一个需要缓存的数据我们都要来指定要放到哪个分区的缓存【缓存的分区（按照业务逻辑类型划分）】
	 * 2、@Cacheable（value = "category") value属性是指定缓存的分区
	 * 指定方法的结果需要缓存，如果缓存中有，则该方法不会执行
	 * 如果缓存中没有，则执行指定方法，并把方法返回结果放入缓存
	 * 3、默认行为：
	 * 1）、如果缓存中有，方法不会调用
	 * 2）、key默认自动生成，缓存的名字（缓存的分区）:: simpleKey（自主生成的key）
	 * 3）、缓存的value值，默认使用jdk序列化机制，将序列化后的数据存到redis，
	 * 4）、默认时间ttl：缓存时间 -1（永久）
	 * 自定义：
	 * 1）、指定生成缓存使用的key：key属性指定，接受一个Spel表达式
	 * 2）、指定缓存的数据的存活时间：需要在配置文件中配置 time-to-live： 存活时间
	 * 3）、将数据保存为json格式
	 * 4、SpringCache的不足：
	 * 1）、读模式：
	 * 缓存穿透：大量查询一个为null的数据。解决：缓存null数据
	 * 缓存击穿：大量请求查询一个缓存中没有的数据。解决：加锁，默认是无加锁的；sync = true（加锁，就解决了当前缓存击穿问题）
	 * 雪崩：大量的key同时过期。解决：加上随机时间。加上过期时间
	 * 2）、写模式（缓存与数据库一致）：
	 * 1）、读写加锁
	 * 2）、引述canal感知到mysql的更新去更新数据库
	 * 3）、读多写少，直接去数据库查就行
	 * 总结：常规数据（读多写少，及时性，一致性要求不高的数据）；完全可以使用spring-cache ：写模式
	 * 特殊数据：特殊设计
	 * @return
	 */
	// value：指定缓存在redis中的分区，key：指明该缓存在该分区的唯一标识
	@Cacheable(value = "category", key = "#root.method.name", sync = true)
	@Override
	public List<CategoryEntity> getLevelOneList() {
		QueryWrapper<CategoryEntity> queryWrapper = new QueryWrapper<>();
		queryWrapper.eq("parent_cid", 0);
		return baseMapper.selectList(queryWrapper);
	}

	@Cacheable(value = "category", key = "#root.methodName", sync = true)
	@Override
	public Map<String, List<Catelog2Vo>> getLevelTwoList() {
		return getDataFromDB();
	}

	/**
	 * TODO 产生堆外内存溢出：OutOfDirectMemoryError
	 * 1)、springboot2.0以后默认使用lettuce作为操作redis的客户端。它使用netty进行网络通信。
	 * 2）、lettuce的bug导致netty堆外内存溢出 -Xmx300m；netty如果没有指定堆外内存，默认使用-Xmx300m
	 * 解决方案：不能使用-Dio.netty.maxDirectMemory只去调大堆外内存
	 * 1）、升级lettuce客户端。2）、切换使用jedis
	 * @return
	 */
	public Map<String, List<Catelog2Vo>> getLevelTwoListOld() {
		/**
		 * 缓存穿透：大量并发恶意查询一个数据库并不存在的值，绕过缓存，造成数据库瞬时访问量过载；
		 * 解决方法：如果从数据库里查询出一个null，随机向缓存中放入一个值
		 * 缓存雪崩：缓存中多个值的过期时间到了，缓存中的许多数据同时消失，突然来了一个多线程高并发导致许多请求查询数据库，造成数据库瞬时的访问过多，崩溃；
		 * 解决方法：动态设置每个值的过期时间，让每个值的过期时间拉开
		 * 缓存击穿：大量请求访问同一个key，缓存中没有该值时，大量请求就会访问数据库，造成数据库的崩溃；
		 * 解决方法：加锁
		 */
		// 从缓存中取出该数据
		String catalogTreeJson = redisTemplate.opsForValue().get("catalogTreeJson");

		if (!StringUtils.isEmpty(catalogTreeJson)) {
			System.out.println("缓存命中。。。");
			// 如果缓存中有数据，就返回缓存中的数据
			Map<String, List<Catelog2Vo>> listMap = JSON.parseObject(catalogTreeJson, new TypeReference<Map<String, List<Catelog2Vo>>>() {
			});
			return listMap;
		}

		// 如果缓存中没数据，就从数据库中查出来，并直接返回
		Map<String, List<Catelog2Vo>> listMap = getLevelTwoListFromDBRedisson();

		return listMap;
	}

	public Map<String, List<Catelog2Vo>> getLevelTwoListFromDBRedisson() {
		Map<String, List<Catelog2Vo>> dataFromDB = null;

		// 获取锁
		RLock lock = redissonClient.getLock("catalogListJSON");

		try {
			// 上锁
			lock.lock();
			dataFromDB = getDataFromDB();

		} catch (Exception e) {
			e.printStackTrace();
		} finally {
			lock.unlock();// 放锁
		}


		return dataFromDB;
	}

	/**
	 * 自定义分布式锁，但是存在锁的过期时间问题
	 * 添加分布式锁，分布式锁最主要是两个原子性的操作：
	 * 一、判断redis中是否有该key存放的值，如果没有则在redis中设置key并设置过期时间
	 * 二、判断当前线程生成的uuid和redis中存放的uuid是否相同，如果相同则删除锁
	 * @return 二级菜单和三级菜单组成的map
	 */
	public Map<String, List<Catelog2Vo>> getLevelTwoListFromDB() {
		// 随机生成一个不会重复的值
		String uuid = System.currentTimeMillis() + UUID.randomUUID().toString();
		// 并把该值以lock为key放入redis中（占坑），
		// 如果该值在redis中已存在，则添加失败（返回false，说明这个这个锁有服务握着），
		// 如果该值在redis中不存在，则添加成功（返回true，说明当前这个服务没人用）
		// 设置过期时间为300毫秒
		Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock", uuid, 300, TimeUnit.MILLISECONDS);

		if (lock) {

			Map<String, List<Catelog2Vo>> data = null;
			try {
				// 到此步表明以在redis中存放了key，占坑成功，查询数据库，返回结果
				data = getDataFromDB();
			} finally {
                /*
                 if redis.call("get",KEYS[1]) == ARGV[1] then return redis.call("del",KEYS[1]) else return 0 end
                 判断redis中存放的value是否是自己放的value，如果是自己放的value，则删除（删除成功返回1），如果不是删除失败（0），
                 该删除方式是原子性的操作
                 */
				String script = "if redis.call('get',KEYS[1]) == ARGV[1] then return redis.call('del',KEYS[1]) else return 0 end";
				Long execute = redisTemplate.execute(new DefaultRedisScript<Long>(script, Long.class), Arrays.asList("lock"), uuid);
			}
			// 最终返回数据
			return data;
		} else {
			// 由于回旋方法过快，会造成栈内存溢出，所以在这睡100毫秒
			try {
				Thread.sleep(100);
			} catch (Exception e) {

			}
			return getLevelTwoListFromDB(); // 回旋操作，类似于synchronized
		}

	}

	public Map<String, List<Catelog2Vo>> getDataFromDB() {

		// 用于解决缓存击穿问题
		//  synchronized (this){
		// 从缓存中取出该数据
        /*
        已经使用了@Cacheable 就不用再查询缓存了
        String catalogTreeJson = redisTemplate.opsForValue().get("catalogTreeJson");

        if (!StringUtils.isEmpty(catalogTreeJson)) {
            // 如果缓存中有数据，就返回缓存中的数据
            Map<String, List<Catelog2Vo>> listMap = JSON.parseObject(catalogTreeJson, new TypeReference<Map<String, List<Catelog2Vo>>>() {
            });
            return listMap;
        }
        */
		System.out.println("缓存未命中。。。查询数据库。。。");
		// 查出所有得菜单分类
		List<CategoryEntity> allEntitys = baseMapper.selectList(null);

		// 获取所有一级分类
		List<CategoryEntity> levelOneList = findCatalogList(allEntitys, 0);
		// 遍历一级分类，并收集成一个Map，key是一级分类的id，value是用于页面显示的二级分类集合
		Map<String, List<Catelog2Vo>> resultMap = levelOneList.stream().map(l1 -> {
			// 一级分类id
			Integer catalog1Id = l1.getCatId();
			// 查出该一级分类下的所有二级分类
            /*QueryWrapper<CategoryEntity> queryWrapper = new QueryWrapper<>();
            queryWrapper.eq("parent_cid", catalog1Id);
            List<CategoryEntity> catalogLevelTwos = baseMapper.selectList(queryWrapper);*/
			// 优化
			List<CategoryEntity> catalogLevelTwos = findCatalogList(allEntitys, l1.getCatId());
			if (catalogLevelTwos != null) {
				// 遍历二级分类，并收集二级分类为集合
				List<Catelog2Vo> catelog2VoList = catalogLevelTwos.stream().map(l2 -> {

					Catelog2Vo catelog2Vo = new Catelog2Vo();
					// 二级分类id
					Integer catalog2Id = l2.getCatId();
					// 设置一级分类id
					catelog2Vo.setCatalog1Id(catalog1Id.toString());
					catelog2Vo.setName(l2.getName());
					catelog2Vo.setId(catalog2Id.toString());
					// 查出该二级分类下的所有一级分类
                    /*QueryWrapper<CategoryEntity> queryWrapper2 = new QueryWrapper<>();
                    queryWrapper2.eq("parent_cid", catalog2Id);
                    List<CategoryEntity> catalog3List = baseMapper.selectList(queryWrapper2);*/
					// 对于之上的优化
					List<CategoryEntity> catalog3List = findCatalogList(allEntitys, l2.getCatId());

					if (catalog3List != null) {
						// 遍历所有三级分类，并封装成一个用于页面显示的vo集合
						List<Catelog2Vo.CataLlog3Vo> catalogLevelThree = catalog3List.stream().map(l3 -> {
							Catelog2Vo.CataLlog3Vo cataLlog3Vo = new Catelog2Vo.CataLlog3Vo();
							cataLlog3Vo.setCatalogId(catalog2Id.toString());
							cataLlog3Vo.setName(l3.getName());
							cataLlog3Vo.setId(l3.getCatId().toString());
							return cataLlog3Vo;
						}).collect(Collectors.toList());
						catelog2Vo.setCatalog3List(catalogLevelThree);
					}
					// 用于收集二级分类
					return catelog2Vo;
				}).collect(Collectors.toList());
				// 用于返回二级分类
				return catelog2VoList;
			}
			return null;
		}).collect(Collectors.toMap(item -> item.get(0).getCatalog1Id(), item -> item));

		// 使用了@Cacheable 注解就不用使用这个东西了
		// 使用JSON原因：JSON字符串可以跨平台使用，跨平台兼容
		// 由于当前代码块被synchronized包着，把当前从数据库查出来的数据直接放入缓存中，将从数据库查询数据和放入缓存的过程变成一个原子性的操作
		//String jsonString = JSON.toJSONString(resultMap);
		// 设置缓存的过期时间为一小时
		// redisTemplate.opsForValue().set("catalogTreeJson", jsonString, 1, TimeUnit.HOURS);

		return resultMap;
		//        }

	}

	/**
	 * 根据parentId查找菜单的下一级分类
	 * @param categoryEntities 所有的菜单
	 * @param parentId         parentId
	 * @return
	 */
	public List<CategoryEntity> findCatalogList(List<CategoryEntity> categoryEntities, Integer parentId) {
		List<CategoryEntity> collect = categoryEntities.stream().filter(item -> item.getParentCid() == parentId).collect(Collectors.toList());
		return collect;
	}

	/**
	 * 通过子id递归查找父Id
	 * @param catId
	 * @param paths
	 */
	private void findParentId(Long catId, List<Long> paths) {
		paths.add(catId);
		CategoryEntity categoryEntity = baseMapper.selectById(catId);

		if (categoryEntity.getParentCid() != 0) {
			findParentId(categoryEntity.getParentCid().longValue(), paths);
		}
	}

	/**
	 * 递归查找子菜单
	 * @param root        需要查找子菜单的菜单
	 * @param allEntities 所有的菜单
	 * @return 查找到子菜单的集合
	 */
	private List<CategoryEntity> findChildren(CategoryEntity root, List<CategoryEntity> allEntities) {

		List<CategoryEntity> finalEntity = allEntities.stream()

				// 当前需要查找的菜单的id 和正在遍历的菜单的父id是否相等 ！！！ Long类型的包装类不可以直接用 == 来判断是否相等！！！
				.filter(entity -> entity.getParentCid().equals(root.getCatId()))
				// 递归查找设置当前菜单下的每一个子菜单的子菜单的集合
				.map(entity -> {
							entity.setChildren(findChildren(entity, allEntities));
							return entity;
							// 对查找的菜单进行排序
						}
						// 把最后的结果收集成集合
				).sorted((menu1, menu2) -> {
					return (menu1.getSort() == null ? 0 : menu1.getSort()) - (menu2.getSort() == null ? 0 : menu2.getSort());
				}).collect(Collectors.toList());

		return finalEntity;

	}

	/**
	 * 更新CategoryRedisCache
	 */
	@Override
	public void updateRedisCache() {
        redisTemplate.delete("category::getLevelOneList");
        redisTemplate.delete("category::getLevelTwoList");
	}
}